跳到主要内容

Redis 持久化

AOF

AOF 日志

  • AOF(Append Only File):Redis 每执行一条命令,就把该命令以追加的方式写入一个文件中,但是指挥记录写操作命令,不会记录读操作命令
  • Redis 先执行写操作,然后再将命令记录到 AOF 日志,这样做有个好处
    1. 避免额外的检查开销:先记录再执行的话,需要对命令进行语法检查,如果不进行语法检查,可能导致恢复时出错;先执行再记录,只有命令成功执行时才会记录,可以保证 AOF 日志中的命令都是正确可执行的
    2. 不会阻塞当前写操作命令的执行:写操作执行成功后才会将命令记录到 AOF 日志中,因此不会阻塞当前写操作
  • 存在风险
    1. 执行写操作和记录日志是两个过程,如果 Redis 宕机时还没来得及将操作写入 AOF 日志,就有数据丢失的可能
    2. 写操作执行成功后才记录日志,不会阻塞当前写操作,但是有可能阻塞后续的操作,因为命令写入日志的操作也是在主进程执行的,如果磁盘 I/O 压力大,导致写入速度慢,进而阻塞后续操作
      写入 AOF
  • AOF 写入过程
    AOF 写入过程

写回策略

  • 三种写回策略

    1. Always:每次执行完写操作,同步将 AOF 日志数据写回硬盘
    2. Everysec:每次执行完写操作,先将命令写入 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区中的内容写回硬盘
    3. No:不由 Redis 控制写回硬盘的时机,交给操作系统控制,即每次执行完写操作,将命令写入 AOF 文件的内核缓冲区,然后由操作系统决定何时将缓冲区内容写回硬盘

    三种写回策略无法完美解决 主进程阻塞减少数据丢失 的问题:

    • Always 策略可以最大程度保证数据不丢失,但是会影响主进程性能(高可靠)
    • No 策略性能较好,但是写回硬盘时间不确定,因此宕机时可能丢失的数据量无法确定(高性能)
    • Everysec 策略较为折中,避免了较大的性能开销,也比较能避免数据丢失(折中)
      写回策略优缺点
  • 策略实现机制
    三种策略的本质就是在控制 fsync() 函数的调用时机,当应用程序向文件写入数据时,内核通常先将数据复制到内核缓冲区,然后排入队列,最后由内核决定何时写入硬盘
    策略实现机制

重写机制

  • AOF 文件随着写操作命令增多,文件会越来越大,过大可能带来性能问题,比如 Redis 重启恢复数据时恢复速度慢,因此 Redis 提供了 AOF 重写机制,AOF 文件超过阈值时会对 AOF 文件重写进行压缩
    重写机制就是在重写是,读取当前数据库内所有的键值对,然后将每一个键值对用一条命令记录到 新的 AOF 文件,等到全部记录完后,使用新的 AOF 文件替换现有的 AOF 文件
    不复用现有 AOF 文件的原因:防止 AOF 文件重写失败,造成对现有 AOF 文件的污染

  • AOF 后台重写
    AOF 文件重写的过程是十分耗时的,因此不能放在主进程中进行,而是由后台子进程 bgrewriteaof 完成,这样做的好处在于:

    • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,避免阻塞主进程
    • 子进程带有主进程的数据副本
      使用子进程而非线程的的原因:多线程之间会共享内存,修改共享内存的数据时需要通过加锁来保证数据的安全,因此会降低性能;而使用子进程的话,在创建子进程时,父子进程是共享内存数据的,但是贡献的内存只能以 只读 的方式,当父子进程任意一方修改共享内存,就会发生写时复制,父子进程各自用用独立的数据副本,不用加锁保证数据安全
  • 子进程如何拥有父进程的数据副本
    主进程通过 fork 系统调用生成 bgrewriteaof 子进程时,操作系统就会把主进程的 页表 复制一份给子进程,页表记录着虚拟地址和物理地址的映射关系,而不会复制物理内存,即两者的虚拟空间不同,但对应的物理空间是同一个
    父子进程页表
    子进程共享父进程的物理内存数据,能够 节省物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读
    当父进程或者子进程对这个内存发起写操作时,CPU 会触发 写保护中断(由于违反只读权限导致),然后操作系统会在 写保护中断处理函数 里进行 物理内存复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为 可读写,最后才会对内存进行写操作,这个过程称为 写时复制(Copy On Write)
    写时复制
    写时复制:发生写操作时,操作系统才会复制物理内存,可以防止 fork 子进程时,由于物理内存数据的复制时间过长导致父进程长时间阻塞
    操作系统复制父进程页表的时候父进程也是阻塞的,但是页表的大小相比于实际物理内存小很多,因此过程比较快,但是如果父进程的内存数据非常大,页表也会很大,此时父进程通过 fork 创建子进程阻塞的时间也越久
    导致阻塞父进程的两个阶段

    • 创建子进程时复制父进程的页表等数据结构,阻塞时间跟页表大小相关,页表越大,阻塞时间越长
    • 创建完子进程后,如果父进程或子进程修改了共享数据,就会发生 写时复制,期间会拷贝物理内存,内存越大,阻塞时间越长

    子进程重写过程中父进程可以正常处理命令,如果此时主进程修改了已经存在的 key-value,就会发生写时复制,但是 只会复制主进程修改的物理内存,没有修改的物理内存仍然和子进程共享,如果这个阶段修改的是一个 bigkey,这时复制的物理内存数据就会比较耗时,有阻塞主进程的风险

  • 父子进程数据不一致
    主进程修改了已经存在的 key-value,此时这个数据在子进程的内存数据和父进程不一致
    Redis 设置了一个 AOF 重写缓冲区来解决这个问题,这个缓冲区在创建 bgrewriteaof 子进程后开始使用,在重写期间,当 Redis 执行完一个写命令后,会同时将这个写命令写入 AOF 缓冲区AOF 重写缓冲区,当子进程完成 AOF 重写工作后,会向主进程发送一条信号,主进程收到信号后会调用一个信号处理函数,完成以下工作

    • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 文件中,使新旧两个 AOF 文件保存的数据库状态一致
    • 将新的 AOF 文件改名,覆盖现有的 AOF 文件

    而在 AOF 重写期间主进程需要执行以下工作:

    • 执行客户端发来的命令
    • 将执行后的写命令追加到 AOF 缓冲区
    • 将执行后的写命令追加到 AOF 重写缓冲区

    AOF 重写数据不一致
    在整个 AOF 重写过程中,除了发生写时复制会对主线程造成阻塞,信号处理函数执行时也会对主线程造成阻塞,其他时候,AOF 重写都不会阻塞主线程

RDB

RDB 快照

  • AOF 文件的内容是 操作命令,而 RDB 快照的内容是 二进制数据
    RDB 快照记录的是 某一个瞬间的内存数据(实际数据),而 AOF 文件记录的是命令操作的日志(非实际数据),因此在数据恢复时,RDB 的效率高于 AOF

  • Redis 提供了两个命令来生成 RDB 文件: savebgsave,他们的区别在于是否在主线程中执行

    • save:在主线程生成 RDB 文件,如果写入时间太长,会阻塞主线程
    • bgsave:创建子进程用于生成 RDB 文件,可以避免主线程阻塞

    RDB 文件的加载工作在服务器启动时自动执行,Redis 没有提供专门用于加载 RDB 文件的命令

  • RDB 的快照是全量快照,每次执行都是把内存中所有的数据记录到磁盘中,是一个比较重的操作,频率太高可能影响 Redis 性能,频率太低,可能在宕机时丢失更多数据,这也是 RDB 快照的缺点

执行快照时的数据修改

  • Redis 执行 bgsave 时继续处理操作命令的关键在于 写时复制技术(Copy-On-Write,COW),在执行 bgsave 命令时会通过 fork() 创建子进程,此时父子进程共享同一片内存数据,同 AOF
  • 父子进程数据不一致
    发生了写时复制后,RDB 快照文件保存的是原本的内存数据,而主线程刚修改的数据只能由下一次快照保存
    因此如果在快照创建完毕后 Redis 崩溃,将会丢失这个期间主线程修改的数据
  • 极端情况
    在执行 RDB 快照持久化期间,所有的共享内存均被修改,此时占用的内存会是原先的两倍,因此在写操作多的场景下需要留意快照过程内存变化,防止内存溢出

AOF + RDB

  • Redis 4.0 提出混合持久化,将 RDB 和 AOF 缓和使用
    aof-use-rdb-preamble yes
  • 混合持久化工作在 AOF 日志重写期间
    在 AOF 重写日志时,fork 出来的子进程会先将共享的内存数据以 RDB 方式写入 AOF 文件,而主线程处理的操作命令会被记录在 AOF 重写缓冲区,重写缓冲区中的增量命令会以 AOF 方式写入 AOF 文件,写入完成后通知主线程用新的 混合持久化文件 替换旧的 AOF 文件
    使用了混合持久化之后,AOF 文件前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
  • 优点
    重启时加载速度快,宕机时丢失数据少

bigkey 对持久化的影响

  • bigkey 占用的内存较大,会导致持久化时对页表和内存数据的复制比较耗时,可能导致主线程阻塞
  • 如何避免 bigkey
    在设计阶段将 bigkey 拆分,或者定时检查是否存在 bigkey,对于可删除的 bigkey 使用 unlink 命令进行异步删除,避免阻塞主线程